package hk.com.carfield.web.simple;

import hk.com.carfield.action.Body;
import hk.com.carfield.action.Plain;
import hk.com.carfield.core.ActionManager;
import hk.com.carfield.core.Context;
import hk.com.carfield.persistance.GenericEntity;
import hk.com.carfield.util.FileUtil;
import hk.com.carfield.util.LangUtil;
import hk.com.carfield.util.LogUtil;
import hk.com.carfield.util.StringUtil;
import hk.com.carfield.util.UrlUtil;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.channels.FileChannel;

import org.apache.commons.io.IOUtils;
import org.simpleframework.http.Request;
import org.simpleframework.http.Response;
import org.simpleframework.http.core.Container;
import org.simpleframework.transport.connect.Connection;
import org.simpleframework.transport.connect.SocketConnection;
import org.simpleframework.util.thread.Scheduler;

import ar.com.pabloa.jmimetypelib.JMimetypeLib;

public final class Controller implements Container {
	public static void main(final String[] list) throws Exception {
		final int defaultPort = 80;
		final String defaultPath = System.getProperty("user.dir");
		File file = new File(new File(defaultPath), "WEB-INF/data");
		int port = defaultPort;
		String dbName = "db";
		try {
			port = Integer.parseInt(list[0]);
			file = new File(list[1]);
			dbName = list[2];
		} catch (final RuntimeException e) {
			// Just parameter haven't set;
			e.printStackTrace();
		}
		final Controller controller = new Controller(
				new hk.com.carfield.core.Context(file, dbName));
		controller.start(port);
	}

	private final hk.com.carfield.core.Context ctx;

	private final JMimetypeLib mimetypeLib = new JMimetypeLib();

	private final Scheduler queue = new Scheduler(5);

	public Controller(final Context ctx) {
		super();
		this.ctx = ctx;
	}

	@Override
	protected void finalize() throws Throwable {
		super.finalize();
		ctx.shutdown();
	}

	public void handle(final Request request, final Response response) {
		queue.execute(new Runnable() {
			public void run() {
				final hk.com.carfield.web.Request req = hk.com.carfield.web.simple.Request
						.getRequest(request, ctx.config);
				try {
					handleRequest(request, req, response);
				} catch (final Throwable e) {
					if (e.getMessage().contains("Stream has been closed")) {
						return;
					}
					if (e.getMessage().contains("Error sending response")) {
						return;
					}
					LogUtil.error(e, ctx.mailMan, null, req);
					LangUtil.exitForOOME(e);
					try {
						response.getPrintStream()
								.append("System error, admin are noted and please visit again tomorrow");
					} catch (final IOException e1) {
						throw new RuntimeException(e1);
					}
				}
			}
		});
	}

	private void handleRequest(final Request req,
			final hk.com.carfield.web.Request request, final Response res)
			throws Exception {
		File location = ctx.config.webRoot;
		final Body body;
		try {
			if (ctx.config.disallow(request.clientIP())) {
				return;
			}

			res.set("Content-Type", "text/html; charset=UTF-8");
			location = UrlUtil.getLocation(ctx, request.getParameter("url"),
					req.getPath().getPath());

			setCacheDate(res, FileUtil.cacheExpire(location));
			ctx.executeTasks(request, location);

			if (FileUtil.notFound(location)) {
				final InputStream is = UrlUtil.resolveInputStream(ctx.config,
						location.getName());
				if (is != null) {
					rawDisplay(res, location, is);
					return;
				}
				body = Plain.notFoundBody(location, ctx);
				res.setCode(404);
			} else if (ctx.config.isSecure(location, request)) {
				body = Plain.forbiddenBody(location, ctx);
				res.setCode(403);
			} else if (request.getBoolean("downloadZip")) {
				res.set("Content-Type", "application/zip");
				res.set("Content-Disposition", "attachment; filename=\""
						+ location.getName() + ".zip");
				final boolean result = FileUtil.zip(res.getOutputStream(),
						location);
				if (result) {
					return;
				} else {
					body = Plain.getErrorBody(new FileNotFoundException(
							"Not file under this location can be zip"),
							location, ctx, request);
				}
			} else if (StringUtil.notBlank(request.getParameter("actioncode"))
					|| request.getBoolean("wrapped") || location.isDirectory()) {
				body = ActionManager.getBody(ctx, request, location);
			} else if (request.getBoolean("thumbnail")) {
				rawDisplay(res, location, new ByteArrayInputStream(
						GenericEntity.getByte(ctx, location)));
				return;
			} else {
				rawDisplay(res, location);
				return;
			}
			if (body.getRedirectTo() != null) {
				res.setCode(302);
				res.set("Location", body.getRedirectTo());
				res.commit();
			} else if (request.getBoolean("xml")) {
				StringUtil.writeXML(request.host(), body.getHead(),
						new OutputStreamWriter(res.getOutputStream()));
			} else {
				ctx.templateMan.mergeTemplate(ctx, body, location, request,
						"UTF-8", res.getOutputStream());
			}
		} catch (final RuntimeException e) {
			LogUtil.error(e, ctx.mailMan, location, request);
			res.setCode(500);
			ctx.templateMan.mergeTemplate(ctx,
					Plain.getErrorBody(e, location, ctx, request), location,
					request, "UTF-8", res.getOutputStream());
		} finally {
			res.close();
		}
	}

	private void rawDisplay(final Response res, final File location)
			throws IOException, FileNotFoundException {
		// TODO set charset with mozilla library
		if (location.isFile()) {
			final InputStream is = new FileInputStream(location);
			rawDisplay(res, location, is);
		} else {
			LogUtil.error(
					new IOException("Try to show raw of invalid location"),
					ctx.mailMan, location, null);
		}
	}

	// TODO change to use NIO ?
	private void rawDisplay(final Response res, final File file,
			final InputStream is) throws IOException {
		res.set("Content-Type", mimetypeLib.getMimeType(file));
		setCacheDate(res, file.lastModified());

		if (is instanceof FileInputStream) {
			final FileInputStream fin = (FileInputStream) is;
			final FileChannel fc = fin.getChannel();
			fc.transferTo(0, fc.size(), res.getByteChannel());
			fc.close();
		} else {
			IOUtils.copy(is, res.getOutputStream());
		}
		is.close();
	}

	private void setCacheDate(final Response res, final long ts) {
		res.setDate("last-modified", ts);
		res.setDate("if-modified-since", ts);
	}

	private void start(final int port) throws IOException, Exception {
		final Connection connection = new SocketConnection(this);
		final SocketAddress address = new InetSocketAddress(port);
		connection.connect(address);
	}

}